/**
 * @date 2017.11.1
 * 封装底层
 */

let mysql = require('mysql'),
  _ = require('lodash'),
  conf = require('../../conf'),
  Log = require('../helper/logger'),
  util = require('../helper/util'),
  tagLogger = new Log.TagLogger('DATABASE-CONN'),
  errCode = require('../../conf/errorCode');

pool = mysql.createPool({
  connectionLimit: conf.mysql.connectionLimit,
  host: conf.mysql.host,
  user: conf.mysql.user,
  password: conf.mysql.password,
  database: conf.mysql.database,
  supportBigNumbers: conf.mysql.supportBigNumbers,
  multipleStatements: conf.mysql.multipleStatements,
});

function getConn(callback) {
  //pool对象调用query方法，底层也是采用getConnection方法
  pool.getConnection(function(err, conn) {
    if (err) {
      tagLogger.error(err);
      callback({
        errNo: errCode.DATABASE_GET_CONNECTION_ERROR,
      });
    } else {
      callback(null, conn);
    }
  });
}

function getByConn(conn, str, ...rest) {
  if (rest.length == 1) {
    let callback = rest[0];
    conn.query(
      {
        sql: str,
        timeout: conf.mysql.timeout,
      },
      (err, rows) => {
        if (err) {
          tagLogger.error(err);
          callback({
            errNo: errCode.DATABASE_QUERY_ERROR,
          });
        } else {
          callback(null, rows);
        }
      },
    );
  } else if (rest.length == 2) {
    let param = rest[0],
      callback = rest[1];
    tagLogger.debug(`getByConn, param[${JSON.stringify(param)}]`);
    conn.query(
      {
        sql: str,
        timeout: conf.mysql.timeout,
      },
      param,
      (err, rows) => {
        if (err) {
          tagLogger.error(err);
          callback({
            errNo: errCode.DATABASE_QUERY_ERROR,
          });
        } else {
          callback(null, rows);
        }
      },
    );
  } else {
    tagLogger.error(`getByConn, rest.length[${rest.length}] unsupport`);
  }
}
let queryByConn = getByConn;

function get(str, param, callback) {
  getConn((err, conn) => {
    if (err) {
      callback(err, null);
    } else {
      getByConn(conn, str, param, (err, rows) => {
        conn.release();
        callback(err, rows);
      });
    }
  });
}
let query = get;

function getStr(str, callback) {
  getConn((err, conn) => {
    if (err) {
      callback(err, null);
    } else {
      getByConn(conn, str, (err, rows) => {
        conn.release();
        callback(err, rows);
      });
    }
  });
}
let queryStr = getStr;

// conn      : connection
// resolveArr: [{onResolve: <function(rslts)>}, ...]
// eg.       : [
//              {onResolve: (rslts) => { rslts.push(123); }},
//              {onResolve: (rslts) => { if(rslts.indexOf(123) > -1) return Promise.reject('contains 123'); }},
//             ]
function transactResolves(conn, resolveArr, callback) {
  resolveArr = resolveArr || [];
  conn.beginTransaction(function(btErr) {
    if (btErr == null) {
      // head promise
      let promise = Promise.resolve([]);
      // body promise
      for (let reso of resolveArr) {
        promise = promise.then(reso.onResolve);
      }
      // foot promise
      promise.then(
        rslts => {
          conn.commit(function(commitErr) {
            conn.release();
            if (commitErr == null) {
              callback(null, rslts);
            } else {
              tagLogger.error(
                `transactResolves, commit error[${commitErr}][${JSON.stringify(
                  commitErr,
                )}]`,
              );
              callback({
                errNo: errCode.DATABASE_COMMIT_ERROR,
              });
            }
          });
        },
        proErr => {
          conn.rollback(function() {
            conn.release();
            tagLogger.error(
              `transactResolves, rollback due to error[${proErr}][${JSON.stringify(
                proErr,
              )}]`,
            );
            callback(proErr, null);
          });
        },
      );
    } else {
      conn.release();
      tagLogger.error(`transactResolves, ${btErr}`);
      callback(
        {
          errNo: errCode.DATABASE_ERROR,
        },
        null,
      );
    }
  });
}

const SKIPPED_RSLT = Object.freeze(
  Object.assign([], {
    affectedRows: 0,
    changedRows: 0,
    insertId: 0,
    isSkippedRslt: true,
  }),
);

// queryArr: [{
//              str    : <string: sql>,
//              onStr  : <string: function(rslts: <array: SQL result list>)>,
//              param  : [...],
//              onParam: <array: function(rslts: <array: SQL result list>)>,
//              onBegin: <bool: function(rslts: <array: SQL result list>)>,
//              onEnd  : <bool | {x: x}: function(rslt: <SQL result>)>
//            },
//            ...]
//  1. `str`参数和`onStr`函数至少要存在1个; `param`参数, `onParam`函数至多存在1个; `onBegin`函数和`onEnd`函数可选
//  2. 当`str`参数和`onStr`函数同时存在时, 优先使用`str`参数
//  3. 当`param`参数和`onParam`函数同时存在时, 优先使用`param`参数
//  4. 当`onBegin`函数:
//      如果返回`true`, 该SQL查询将被执行;
//      如果返回`false`, 该SQL查询将被跳过, 结果集中增加`SKIPPED_RSLT`, 事务将继续执行;
//      如果返回`Object`, 将导致事务回滚(rollback)
//  5. 当`onEnd`函数:
//      如果返回`false`或者`Object`, 表示验证失败, 这将导致事务回滚(rollback);
//      其他情况, 事务将继续执行
// [{str: 'SELECT * FROM tbl1 WHERE id = ? OR id = ?', param: [3, 5] }]
// [{str: 'SELECT * FROM tbl1 WHERE id = ? OR id = ?', param: [3, 5], onEnd: (rslt)=>{return true;} }]
// [{str: 'SELECT * FROM tbl1 WHERE id = ? OR id = ?', param: [3, 5], onEnd: (rslt)=>{return {errNo: FAIL_DUE_TO_NO_LOCK_ERROR};} }]
// [{str: 'SELECT * FROM tbl1 WHERE id = ? OR id = ?', param: [3, 5], onBegin: (rslts)=>{return true;}, onEnd: (rslt)=>{return true;} }]
function getBatch(queryArr, callback) {
  queryArr = queryArr || [];
  let resolveArr = [];

  getConn(function(poolErr, conn) {
    if (poolErr == null) {
      for (let q of queryArr) {
        // body promise
        resolveArr.push({
          onResolve: rslts => {
            let def = util.defer(),
              beginRet = true;
            if (
              q.onBegin == null ||
              ((beginRet = q.onBegin(rslts)) && !_.isObject(beginRet))
            ) {
              //conn str params
              getByConn(
                conn,
                q.str || (q.onStr && q.onStr(rslts)),
                q.param || (q.onParam && q.onParam(rslts)) || null,
                (queryErr, queryRslt) => {
                  if (queryErr == null) {
                    let endRet = null;
                    if (
                      q.onEnd == null ||
                      ((endRet = q.onEnd(queryRslt)) && !_.isObject(endRet))
                    ) {
                      rslts.push(queryRslt);
                      def.resolve(rslts);
                    } else {
                      def.reject(
                        Object.assign(
                          {
                            errNo: errCode.DATABASE_BATCH_QUERY_ON_END_ERROR,
                          },
                          _.isObject(endRet) ? endRet : null,
                        ),
                      );
                    }
                  } else {
                    def.reject(queryErr);
                  }
                },
              );
            } else {
              if (_.isObject(beginRet)) {
                def.reject(
                  Object.assign(
                    {
                      errNo: errCode.DATABASE_BATCH_QUERY_ON_BEGIN_ERROR,
                    },
                    beginRet,
                  ),
                );
              } else {
                rslts.push(SKIPPED_RSLT);
                def.resolve(rslts);
              }
            }
            return def.promise;
          },
        });
      }

      transactResolves(conn, resolveArr, callback);
    } else {
      callback(poolErr, null);
    }
  });
}
let queryBatch = getBatch;

module.exports = {
  getConn,
  transactResolves,
  get,
  getStr,
  getBatch,
  query,
  queryStr,
  queryBatch,
};
